// TODO: CODE REVIEW & CLEANUP!
//
// WindowsService.cs
//
// Implements the entry point to the HomeUX Windows service.
//

using System;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.ServiceProcess;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using HomeUX.Properties;
using HomeUX.Utilities;

namespace HomeUX.Server
{

public class HomeUXWindowsService : ServiceBase, ILogDestination
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
	// Private Fields
	//

    /// <summary>
    /// The name of the "on-(computer)-EncryptedPassword" attribute, where "(computer)" is the
    /// lowercase form of the computer name.
    /// </summary>
    static string _encryptedPasswordAttributeName = String.Format(
        "on-{0}-EncryptedPassword", Util.ComputerName.ToLower());

	/// <summary>
    /// Indicates if we're running in console mode (instead of under the Service Control Manager).
	/// </summary>
	bool _consoleMode = false;

	/// <summary>
	/// The full path to the directory containing the client application.
	/// </summary>
	string _clientDirPath;

    /// <summary>
    /// This services's primary work queue.  Most service operations consist of short actions that
    /// execute on this work queue's thread.
    /// </summary>
    WorkQueue _workQueue = new WorkQueue("HomeUXWorkQueue");

    PolicyFileServer _policyFileServer;

    SyncServer _syncServer;

    HttpServer _httpServer;

    LogFileTraceListener _logFileTraceListener;

    ///////////////////////////////////////////////////////////////////////////////////////////////
	// Private Methods
	//

    /// <summary>
    /// The main entry point for the process.
    /// </summary>
    ///
    /// <param name="args">Command-line arguments.</param>
    ///
    public static void Main(string[] args)
    {
        try
        {
            // parse/interpret command-line arguments (see ReadMe.txt)
            bool consoleMode = false;
            Util.IsShowHighlightOnly = true;
            string logPath = null;
            ArgumentEnumerator argEnum = new ArgumentEnumerator(args);
            bool runService = true;
            while (argEnum.MoveNext())
            {
                if (argEnum.CurrentIsOption("Console"))
                    consoleMode = true;
                else
                if (argEnum.CurrentIsOption("Verbose"))
                    Util.IsShowHighlightOnly = false;
                else
                if (argEnum.CurrentIsOption("SetPassword"))
                {
                    SetPassword(argEnum.GetNext());
                    runService = false;
                }
#if DEBUG && UNITTESTS
                else
                if (argEnum.CurrentIsOption("TestArgumentEnumerator"))
                {
                    ArgumentEnumerator_Test.RunAllTests();
                    return;
                }
#endif
                else
                if (argEnum.CurrentIsOption("Log"))
                    logPath = argEnum.GetNext();
                else
                    throw new UsageException();
            }

            if (!runService)
                return;

            // create the service implementation object <service>
            HomeUXWindowsService service = new HomeUXWindowsService();
            service._consoleMode = consoleMode;

            if (logPath != null)
            {
                service._logFileTraceListener = new LogFileTraceListener(service._workQueue, logPath);
                Debug.Listeners.Add(service._logFileTraceListener);
            }

            // start the service
            if (consoleMode)
            {
                // send debug output to the console
                Debug.Listeners.Add(new ConsoleTraceListener());

                // run the service in console mode
                Console.WriteLine(Resources.ConsoleModeInstructions);
                service.OnStart(new string[0]);
                bool quit = false;
                while (!quit)
                {
                    ConsoleKeyInfo consoleKey = Console.ReadKey(true);
                    switch (consoleKey.Key)
                    {
                        case ConsoleKey.C:
                            Console.Clear();
                            if (service._logFileTraceListener != null)
                                service._logFileTraceListener.Clear();
                            break;

                        case ConsoleKey.Q:
                            quit = true;
                            break;
                        case ConsoleKey.F:
                            WriteConsoleHelpMessage("Filter mode on (only highlighted tracing shown)");
                            Util.IsShowHighlightOnly = true;
                            break;
                        case ConsoleKey.V:
                            WriteConsoleHelpMessage("Verbose mode on (all tracing shown)");
                            Util.IsShowHighlightOnly = false;
                            break;
                        case ConsoleKey.OemMinus:
                            WriteConsoleHelpMessage("{0} {1:h:mm:ss tt} {0}",
                                new String('-', 33), DateTime.Now);
                            break;
                        default:
                            WriteConsoleHelpMessage(
                                "HomeUX Console Mode: C=Clear F=Filter V=Verbose -=Line Q=Quit");
                            break;
                    }
                }
                service.OnStop();
                return;
            }
            else
            {
                // start the service conventionally (using the SCM)
                ServiceBase.Run(new ServiceBase[] { service });
            }
        }
        catch (UsageException)
        {
            Console.WriteLine("Console mode usage:  HomeUX.exe /Console [/Verbose]");
            Console.WriteLine("To set the password: HomeUX.exe /SetPassword <password>");
        }
        catch (FatalException ex)
        {
			Console.WriteLine(ex.Message);
        }
    }

    static void WriteConsoleHelpMessage(string format, params object[] args)
    {
        using (Util.SetConsoleForegroundColor(ConsoleColor.Yellow))
            Console.Write(format, args);
        Console.WriteLine();
    }

    /// <summary>
    /// Initializes an instance of this class.
    /// </summary>
    ///
    public HomeUXWindowsService()
    {
		// initialize the <ServiceName> property
        ServiceName = "HomeUX";

		// initialize <_clientDirPath>
        _clientDirPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location),
            @"..\..\..\Client"));
		if (!Directory.Exists(_clientDirPath))
			throw new FatalException("TODO");
    }

    /// <summary>
    /// Sets the HomeUX server password to the given password.
    /// </summary>
    ///
    /// <param name="password">The new password.</param>
    ///
    static void SetPassword(string password)
    {
        // encrypt <password> using the current user's credentials, and set <encryptedPassword> to
        // the result
        string encryptedPassword = Util.ProtectString(password, DataProtectionScope.LocalMachine);

        // open <config> onto HomeUX.exe.config; set <configurationFilePath> to the
        // location of HomeUX.exe.config
        Configuration config = ConfigurationManager.OpenExeConfiguration(
            ConfigurationUserLevel.None);
        string configurationFilePath = config.FilePath;
        if (!config.HasFile)
        {
            throw new FatalException("Configuration file not found: {0}",
                configurationFilePath);
        }

        // set the password within <configurationFilePath>; this is done textual substitution
        // rather than by XML operations so that spacing in the .config file -- even between
        // attributes in XML elements -- is preserved, but we use XML operations to confirm that
        // the operation succeeded
        try
        {
			// make sure on-(computer)-EncryptedPassword="..." already exists in the .config file
			XDocument doc = XDocument.Load(configurationFilePath);
            GetEncryptedPasswordFromConfigFile(doc, configurationFilePath);

            // read the file into <fileContents>
            string fileContents = File.ReadAllText(configurationFilePath, Encoding.UTF8);

            // make sure there's only one occurrence of on-(computer)-EncryptedPassword="..."
            var regex = new Regex(String.Format("{0}=\"[A-Za-z0-9+/=]*\"", _encryptedPasswordAttributeName));
            MatchCollection matches = regex.Matches(fileContents);
            if (matches.Count != 1)
            {
                throw new FatalException(
                    "Expected to find exactly one occurrence of Encoding=\"{0}\" anywhere in the file (even in comments), but found {0}",
                    matches.Count);
            }

            // replace on-(computer)-EncryptedPassword="..." with the new password; here we assume that no XML
            // encoding is required since <encryptedPassword> is a Base64 string
            fileContents = regex.Replace(fileContents, String.Format("{0}=\"{1}\"",
                _encryptedPasswordAttributeName, encryptedPassword));

            // check that the replacement in <fileContents> was successful
            doc = XDocument.Parse(fileContents);
            string foundEncryptedPassword = GetEncryptedPasswordFromConfigFile(doc, null);
            if (foundEncryptedPassword != encryptedPassword)
                throw new FatalException("Textual update of XML file failed");

            // write <fileContents> back to the .config file
            File.WriteAllText(configurationFilePath, fileContents, Encoding.UTF8);

            // feedback to user
            Console.WriteLine("The encrypted password was updated in \"{0}\" for computer \"{1}\".",
                configurationFilePath, Util.ComputerName.ToLower());
        }
        catch (Exception ex)
        {
            if ((ex is IOException) || (ex is UnauthorizedAccessException) ||
				(ex is FatalException))
            {
                throw new FatalException(
                    "Failed to update configuration file \"{0}\": {1}",
                    configurationFilePath, ex.Message);
            }
            else
                throw;
        }
    }

    /// <summary>
    /// Reads the value of the on-(computer)-EncryptedPassword="..." attribute from the .config file.  Also
    /// ensures the file is encoded using UTF-8 (since that's required by other methods).
    /// </summary>
    ///
    /// <param name="doc">An <n>XDocument</n> open onto the .config file.</param>
    ///
    /// <param name="path">The path to the .config file, used for error messages.  If
    ///     <pr>path</pr> is <n>null</n>, then on error <n>null</n> will be returned instead of
    ///     throwing an exception.</param>
    ///
    /// <exception cref="FatalException">
    /// The attribute on-(computer)-EncryptedPassword="..." could not be found, or some other error occurred
    /// while trying to access the .config file.
    /// </exception>
    ///
	static string GetEncryptedPasswordFromConfigFile(XDocument doc, string path)
	{
        // check for UTF-8 encoding
        if (doc.Declaration.Encoding != "utf-8")
        {
			if (path == null)
				return null;
            throw new FatalException(
                "Password setting requires that the configuration file specify encoding=\"utf-8\": {0}",
				path);
        }

        // set <configurationElement> to the "<configuration>" element
        XElement configurationElement = doc.Element("configuration");
        if (configurationElement == null)
        {
			if (path == null)
				return null;
            throw new FatalException(
                "No <configuration> element found in configuration file: {0}",
                path);
        }

        // set <rootSettingsElement> to the "<HomeUXSettings>" element
        XElement rootSettingsElement = configurationElement.Element("HomeUXSettings");
        if (rootSettingsElement == null)
        {
			if (path == null)
				return null;
            throw new FatalException(
                "No <HomeUXSettings> element found within <configuration> in configuration file: {0}",
                path);
        }

        // set <generalSettingsElement> to the "<generalSettings>" element
        XElement generalSettingsElement = rootSettingsElement.Element("GeneralSettings");
        if (generalSettingsElement == null)
        {
			if (path == null)
				return null;
            throw new FatalException(
                "No <GeneralSettings> element found within <HomeUXSettings> in configuration file: {0}",
                path);
        }

        // set <attribute> to the "on-(computer)-EncryptedPassword" attribute
        XAttribute attribute = generalSettingsElement.Attribute(_encryptedPasswordAttributeName);
        if (attribute == null)
        {
			if (path == null)
				return null;
            throw new FatalException(
                "No {0}=\"...\" was found within <GeneralSettings> in configuration file: {1}; for password setting, the \"{0}\" attribute must be present already, but it can be empty ({0}=\"\")",
                _encryptedPasswordAttributeName, path);
        }

        // return the value of the "on-(computer)-EncryptedPassword" attribute
		return attribute.Value;
	}

    /// <summary>
    /// Starts the service.
    /// </summary>
    ///
    /// <param name="args">Command-line arguments.</param>
    ///
    protected override void OnStart(string[] args)
    {
        string configurationFilePath = null;
        try
        {
            LogInfo(Resources.ServiceStarting);

            // open <config> onto HomeUX.exe.config; set <m_configurationFilePath> to the
            // location of HomeUX.exe.config
            Configuration config =
                    ConfigurationManager.OpenExeConfiguration(
                    ConfigurationUserLevel.None);
            configurationFilePath = config.FilePath;
            if (!config.HasFile)
            {
                // no configuration file found -- this is a fatal exception because
                // communication with clients is impossible without a configuration file
                throw new FatalException("TODO: Service_Fatal_ConfigFileNotFound",
                    configurationFilePath);
            }

            // create the sync server, which implements the HomeUX sync protocol
            _syncServer = new SyncServer()
            {
                WorkQueue = _workQueue,
                LogDestination = this
            };
            try
            {
                using (var textReader = new StreamReader(configurationFilePath))
                    _syncServer.ReadConfigurationFile(textReader, configurationFilePath);
            }
            catch (IOException)
            {
                throw new FatalException("Cannot open configuration file: {0}",
                    configurationFilePath);
            }

            _syncServer.ReadGeneralSettings();

            // start the policy file server, which returns information to Silverlight clients about
            // what socket connections are permitted
            _policyFileServer = new PolicyFileServer()
            {
                WorkQueue = _workQueue,
                GrantToPortNumber = _syncServer.PortNumber
            };
            _policyFileServer.Start();

            _syncServer.Start();

            // start the HTTP listener, which responds to HTTP requests
            _httpServer = new HttpServer()
            {
                PortNumber = 80,
                HttpPath = "HomeUX",
                LocalPath = _clientDirPath
            };
            _httpServer.Start();

            LogInfo(Resources.ServiceStarted);
        }
        catch (HomeUX.DriverModel.ConfigurationException ex)
        {
            if (!HandleException(ex.ToFatalException(
                    configurationFilePath ?? "<configuration file not found>")))
                throw;
        }
#if !NOCATCH
        catch (Exception ex)
        {
            if (!HandleException(ex))
                throw;
        }
#endif
        finally
        {
        }
    }

    /// <summary>
    /// Stops the service.
    /// </summary>
    ///
    protected override void OnStop()
    {
        try
        {
            LogInfo(Resources.ServiceStopping);

            // stop the work queue -- do this before stopping the servers below so that subsequent
            // work items that get queued will be ignored
            if (_workQueue != null)
            {
                _workQueue.Dispose();
                _workQueue = null;
            }

            // stop the policy file server
            if (_policyFileServer != null)
            {
                _policyFileServer.Stop();
                _policyFileServer = null;
            }

            // stop the sync server
            if (_syncServer != null)
            {
#if false
                // the WorkQueue is owned by the application, not by SyncServer, so the application must
                // be responsible for cleanup; the cleanup order is:
                //   1. call WorkQueue.Dispose; subsequent work items that are queued will not be executed;
                //   2. stop SyncServer;
                //   3. set _syncServer.WorkQueue to null to disconnect events;
                //   4. set _syncServer itself to null
                _syncServer.WorkQueue.Dispose();
#endif
                _syncServer.Stop();
                _syncServer.WorkQueue = null;
                _syncServer = null;
            }

            // stop the HTTP server
            if (_httpServer != null)
            {
                _httpServer.Stop();
                _httpServer = null;
            }

            // stop the log file trace listener, if any
            if (_logFileTraceListener != null)
            {
                _logFileTraceListener.Flush();
                _logFileTraceListener.Dispose();
                _logFileTraceListener = null;
            }

            LogInfo(Resources.ServiceStopped);
        }
#if !NOCATCH
        catch (Exception ex)
        {
            if (!HandleException(ex))
                throw;
        }
#endif
        finally
        {
        }
    }

    /// <summary>
    /// Logs a given exception.  Returns <n>true</n> if the caller can safely ignore the exception
    /// or <n>false</n> if the caller should re-throw the exception.
    /// </summary>
    ///
    /// <param name="ex">The exception.</param>
    ///
    public bool HandleException(Exception ex)
    {
        // handle NonfatalException
        if (ex is NonfatalException)
        {
            LogWarning("Nonfatal exception in HomeUX: {0}", Util.FormatExceptionMessage(ex, true));
            return true;
        }

        // handle FatalException
        if (ex is FatalException)
        {
            LogError("Fatal exception in HomeUX: {0}", Util.FormatExceptionMessage(ex, true));
            Environment.Exit(1);
            return true;
        }

        // handle any other exception for which we don't need a stack trace
        if ((ex is HomeUX.DriverModel.ConfigurationException) ||
            (ex is ConfigurationErrorsException))
        {
            LogError("Fatal {0} in HomeUX: {1}", ex.GetType().Name,
                Util.FormatExceptionMessage(ex, true));
            Environment.Exit(1);
            return true;
        }

        // this exception is a coding bug -- include a stack trace
        LogError("Unexpected exception in HomeUX: {0}", ex);

        // tell the caller to re-throw the exception
        return false;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
	// Public Event Logging Methods
	//
	// Note: These methods don't "lock (Lock)" since the only fields they access are either
	// read-only fields initialized during HomeUXService initialization (before new threads are
	// created) or are protected using Interlocked.Increment().
	//

	/// <summary>
	/// Logs an event to the event log.
	/// </summary>
	///
	/// <param name="type">The type of log entry.</param>
	///
	/// <param name="description">The message to log.</param>
	///
	static private void LogEventString(EventLogEntryType type, string description)
	{
        Util.TraceBlock(true, description, "{0} Event", type);

		// log to the event log
		using (EventLog log = new EventLog())
		{
			// use event source name "HomeUX" because that's the name the ServiceBase base class
            // will use when it logs e.g. start/stop events, and we want to be consistent with it
			log.Source = Resources.EventLogSource;

			// write the event log entry
			log.WriteEntry(description, type);
		}
	}

	/// <summary>
	/// Formats a message and logs it to the event log.
	/// </summary>
	///
	/// <param name="type">The type of log entry.</param>
	///
	/// <param name="format">The format string for the message to log.  If <pr>args</pr> is
	/// 	zero-length, <pr>format</pr> is used as-is, without formatting.</param>
	///
	/// <param name="args">Arguments for format string.</param>
	///
	/// <returns>
	/// The text of the logged message.
	/// </returns>
	///
	public string LogEvent(EventLogEntryType type, string format, params object[] args)
	{
		string text = ((args.Length == 0) ? format : String.Format(format, args));
		LogEventString(type, text);
        return text;
	}

	/// <summary>
	/// Formats an information message and logs it to the event log.
	/// </summary>
	///
	/// <param name="format">The format string for the message to log.  If <pr>args</pr> is
	/// 	zero-length, <pr>format</pr> is used as-is, without formatting.</param>
	///
	/// <param name="args">Arguments for format string.</param>
	///
	/// <returns>
	/// The text of the logged message.
	/// </returns>
	///
	public string LogInfo(string format, params object[] args)
	{
        return LogEvent(EventLogEntryType.Information, format, args);
	}

	/// <summary>
	/// Formats an warning message and logs it to the event log.
	/// </summary>
	///
	/// <param name="format">The format string for the message to log.  If <pr>args</pr> is
	/// 	zero-length, <pr>format</pr> is used as-is, without formatting.</param>
	///
	/// <param name="args">Arguments for format string.</param>
	///
	/// <returns>
	/// The text of the logged message.
	/// </returns>
	///
	public string LogWarning(string format, params object[] args)
	{
        return LogEvent(EventLogEntryType.Warning, format, args);
    }

	/// <summary>
	/// Formats an error message and logs it to the event log.
	/// </summary>
	///
	/// <param name="format">The format string for the message to log.  If <pr>args</pr> is
	/// 	zero-length, <pr>format</pr> is used as-is, without formatting.</param>
	///
	/// <param name="args">Arguments for format string.</param>
	///
	/// <returns>
	/// The text of the logged message.
	/// </returns>
	///
	public string LogError(string format, params object[] args)
	{
        return LogEvent(EventLogEntryType.Error, format, args);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Nested Types
    //
#if false
	/// <summary>
	/// A <n>TraceListener</n> which writes to the console.
	/// </summary>
	///
    class ConsoleTraceListener : TraceListener
    {
        public override void Write(string message)
        {
			Console.Write(message);
        }

        public override void WriteLine(string message)
        {
			Console.WriteLine(message);
        }
    }
#endif
    /// <summary>
    /// A <n>TraceListener</n> which writes to a log file.
    /// </summary>
    ///
    class LogFileTraceListener : TraceListener, IDisposable
    {
        object _lock = new object();
        WorkQueue _workQueue;
        string _logPath;
        FileStream _fileStream;
        StreamWriter _streamWriter;
        WorkItem _flushWorkItem;
        
        public LogFileTraceListener(WorkQueue workQueue, string logPath)
        {
            _workQueue = workQueue;
            _logPath = logPath;
            InternalOpen();
        }

        public void Clear()
        {
            lock (_lock)
            {
                Flush();
                InternalClose();
                try
                {
                    if (File.Exists(_logPath))
                        File.Delete(_logPath);
                }
                catch (Exception ex)
                {
                    if ((ex is IOException) || (ex is NotSupportedException) ||
                        (ex is UnauthorizedAccessException))
                    {
                        Console.WriteLine("Error deleting log file \"{0}\": {1}", _logPath, ex.Message);
                        return;
                    }
                }
                InternalOpen();
            }
        }

        void InternalOpen()
        {
            lock (_lock)
            {
                try
                {
                    _fileStream = new FileStream(_logPath, FileMode.Create, FileAccess.Write, FileShare.Read);
                    _streamWriter = new StreamWriter(_fileStream);
                    FlushTimerCallback(); // initializes <_flushWorkItem>
                }
                catch (Exception ex)
                {
                    if ((ex is IOException) || (ex is NotSupportedException) ||
                        (ex is UnauthorizedAccessException))
                    {
                        Console.WriteLine("Error writing to log file \"{0}\": {1}", _logPath, ex.Message);
                        InternalClose();
                    }
                }
            }
        }

        void InternalClose()
        {
            lock (_lock)
            {
                if (_flushWorkItem != null)
                {
                    _flushWorkItem.Abort();
                    _flushWorkItem = null;
                }
                if (_streamWriter != null)
                {
                    _streamWriter.Dispose();
                    _streamWriter = null;
                }
                if (_fileStream != null)
                {
                    _fileStream.Dispose();
                    _fileStream = null;
                }
            }
        }

        protected override void Dispose(bool disposing)
        {
            InternalClose();
        }

        void FlushTimerCallback()
        {
            Flush();
            if (_streamWriter != null)
                _flushWorkItem = _workQueue.Add(200, FlushTimerCallback);
        }

        public override void  Flush()
        {
            lock (_lock)
            {
                if ((_streamWriter != null) && (_fileStream != null))
                {
                    _streamWriter.Flush();
                    _fileStream.Flush();
                }
            }
        }

        // see also: FileLogTraceListener
        public override void Write(string message)
        {
            lock (_lock)
            {
                if (_streamWriter != null)
                    _streamWriter.Write(message);
            }
        }

        public override void WriteLine(string message)
        {
            lock (_lock)
            {
                if (_streamWriter != null)
                    _streamWriter.WriteLine(message);
            }
        }
    }
}

}

